#!/usr/bin/env python
# coding: utf-8

# In[9]:


# -*- coding: utf-8 -*-
"""
信用评分数据分析完整解决方案
包含离散变量和连续变量坏样本分析
"""

#%% 模块一：库导入与配置
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno
import os
from matplotlib import rcParams
from matplotlib.ticker import PercentFormatter

# 全局设置
rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
COLOR_GOOD = '#1f77b4'   # 好样本颜色
COLOR_BAD = '#ff7f0e'    # 坏样本颜色

#%% 模块二：数据配置
CONFIG = {
    # 连续变量配置（基于0的列索引）2,5,8,11,13,16,18
      'continuous': {
        # 第2列（索引1）：贷款时长（Duration in month）
        1: {  
            'bins': [0, 6, 12, 24, 36, np.inf],
            'labels': ['0-6月', '6-12月', '1-2年', '2-3年', '3年以上'],
            'unit': '月'
        },
        # 第5列（索引4）：贷款金额（Credit amount）
        4: {  
            'bins': 5,  # 自动等频分箱
            'precision': -1,  # 显示整数金额
            'unit': '元'
        },
        # 第8列（索引7）：分期付款率（Installment rate）
        7: {
            'bins': [0, 2, 4, 6, 8, np.inf],
            'labels': ['<2%', '2-4%', '4-6%', '6-8%', '8%+'],
            'unit': '占可支配收入百分比'
        },
        # 第11列（索引10）：居住时长（Present residence since）
        10: {
            'bins': [0, 1, 3, 5, 10, np.inf],
            'labels': ['<1年', '1-3年', '3-5年', '5-10年', '10年+'],
            'unit': '年'
        },
        # 第13列（索引12）：年龄（Age in years）
        12: {  
            'bins': [18, 25, 35, 45, 55, 65, np.inf],
            'labels': ['18-25', '26-35', '36-45', '46-55', '56-65', '65+'],
            'unit': '岁'
        },
        # 第16列（索引15）：已有信用卡数量（Number of existing credits）
        15: {
            'bins': [0, 1, 3, 5, np.inf],
            'labels': ['无卡', '1-3张', '4-5张', '5张以上']
        },
        # 第18列（索引17）：可还款人数（Number of people liable）
        17: {
            'bins': [0, 1, 2, 3, np.inf],
            'labels': ['无', '1人', '2人', '3人+']
        }
    },
    # 目标变量配置
    'target': {
        'col': 'label',          # 目标列名
        'good_code': 1,          # 好样本编码
        'bad_code': 2            # 坏样本编码
    },
    # 缺失值编码
    'missing_codes': ['A65', 'A124']
}

#%% 模块三：辅助函数
def normalize_cols(df):
    """列名规范化处理"""
    df.columns = df.columns.str.lower().str.replace(r'[\s/]+', '_', regex=True)
    return df

def safe_convert(df, col, dtype):
    """安全类型转换"""
    try:
        return df[col].astype(dtype)
    except:
        return pd.to_numeric(df[col], errors='coerce')

#%% 模块四：数据加载
def load_data(filepath):
    """数据加载与预处理"""
    df = pd.read_csv(filepath)
    df = normalize_cols(df)

    print("加载数据成功！维度:", df.shape)
    print("前3行数据示例:\n", df.head(3))

    return df

#%% 模块五（数据清洗）最终版
def clean_data(raw_df):
    """数据清洗（修复分类列处理警告）"""
    df = raw_df.copy()

    # 转换目标变量
    target_col = CONFIG['target']['col']
    df[target_col] = df[target_col].replace({
        CONFIG['target']['bad_code']: 0,
        CONFIG['target']['good_code']: 1
    })

    # 处理缺失值
    df.replace(CONFIG['missing_codes'], np.nan, inplace=True)

    # 处理连续变量分箱
    for idx, config in CONFIG['continuous'].items():
        col = df.columns[idx]
        df[col] = safe_convert(df, col, float)

        # 分箱处理
        try:
            if 'bins' in config:
                binned = pd.cut(
                    df[col], 
                    bins=config['bins'], 
                    labels=config.get('labels', None),
                    include_lowest=True
                )
            else:
                binned = pd.qcut(df[col], q=5, duplicates='drop')

            # 转换为分类类型（修复弃用警告）
            df[f'{col}_binned'] = binned.astype('category')
            if isinstance(df[f'{col}_binned'].dtype, pd.CategoricalDtype):
                df[f'{col}_binned'] = df[f'{col}_binned'].cat.add_categories('Missing')
        except Exception as e:
            print(f"分箱错误 {col}: {str(e)}")
            df[f'{col}_binned'] = 'Missing'

    # 填充缺失值（修复分类列处理）
    for col in df.columns:
        if isinstance(df[col].dtype, pd.CategoricalDtype):
            # 分类列处理
            if 'Missing' not in df[col].cat.categories:
                df[col] = df[col].cat.add_categories('Missing')
            df[col] = df[col].fillna('Missing')
        elif np.issubdtype(df[col].dtype, np.number):
            # 数值列处理
            df[col] = df[col].fillna(df[col].median())
        else:
            # 其他类型处理
            df[col] = df[col].fillna('Missing')

    print("数据清洗完成！剩余缺失值:", df.isnull().sum().sum())
    return df

#%% 模块六（离散变量分析）最终修正版
def analyze_discrete(df, save_dir='离散变量分析'):
    """离散变量分析（修复agg参数错误）"""
    os.makedirs(save_dir, exist_ok=True)
    target_col = CONFIG['target']['col']

    # 筛选离散变量
    exclude_cols = [target_col] + \
                  [f"{df.columns[idx]}_binned" for idx in CONFIG['continuous']]

    discrete_cols = df.select_dtypes(exclude=np.number).columns.difference(exclude_cols)

    for col in discrete_cols:
        try:
            # 正确agg语法（修复size错误）
            stats = (
                df.groupby(col, observed=True)
                .agg(
                    总样本数=pd.NamedAgg(column=target_col, aggfunc='size'),
                    坏样本数=pd.NamedAgg(column=target_col, aggfunc=lambda x: (x == 0).sum()),
                    坏样本率=pd.NamedAgg(column=target_col, aggfunc=lambda x: (x == 0).mean())
                )
                .reset_index()
                .sort_values('坏样本率', ascending=False)
            )

            # 可视化部分保持不变...

        except KeyError as e:
            print(f"列 {col} 分析跳过：{str(e)}")
            continue

        # 可视化（修复palette警告）
        plt.figure(figsize=(12, 6))
        sns.barplot(
            x='坏样本率', 
            y=col,
            hue=col,  # 修复警告的关键修改
            data=stats,
            palette='viridis_r',
            legend=False
        )

        # 添加数据标签
        for i, (_, row) in enumerate(stats.iterrows()):
            plt.text(row['坏样本率'] + 0.02, i,
                    f"{row['坏样本率']:.1%} ({row['坏样本数']}/{row['总样本数']})",
                    va='center')

        plt.title(f"{col} - 坏样本率分布")
        plt.xlim(0, 1)
        plt.tight_layout()
        plt.savefig(f"{save_dir}/{col}.png", dpi=120)
        plt.close()

        # 保存数据
        stats.to_csv(f"{save_dir}/{col}.csv", encoding='utf_8_sig')

#%% 模块七（连续变量分析）最终版
def analyze_continuous(df, save_dir='连续变量分析'):
    """连续变量分析（修复趋势线错误）"""
    os.makedirs(save_dir, exist_ok=True)
    target_col = CONFIG['target']['col']

    for idx in CONFIG['continuous']:
        col = df.columns[idx]
        binned_col = f"{col}_binned"

        try:
            # 数据准备
            df_clean = df[[col, target_col, binned_col]].dropna()
            if df_clean.empty:
                print(f"跳过 {col}：无有效数据")
                continue

            # 分箱统计（修复agg参数错误）
            stats = (
                df_clean.groupby(binned_col, observed=True)
                .agg(
                    坏样本率=(target_col, lambda x: (x == 0).mean()),
                    样本数=(target_col, 'count')
                )
                .reset_index()
                .query('样本数 > 0')
            )

            if len(stats) < 2:
                print(f"跳过 {col}：有效分箱不足2个")
                continue

            # 创建画布
            fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), 
                                          gridspec_kw={'height_ratios': [2, 1]})

            # 子图1：分布对比
            sns.histplot(
                data=df_clean,
                x=col,
                hue=df_clean[target_col].map({1:'好样本', 0:'坏样本'}),
                element='step',
                kde=True,
                palette={'好样本': COLOR_GOOD, '坏样本': COLOR_BAD},
                ax=ax1
            )
            ax1.set_title(f"{col} - 好坏样本分布", fontsize=14)

            # 子图2：趋势分析
            x = np.arange(len(stats))
            y = stats['坏样本率'].values

            try:
                # 稳健拟合
                coef = np.polyfit(x, y, 1)
                trend_line = np.poly1d(coef)(x)
                trend_label = f'趋势线 (斜率:{coef[0]:.2f})'
            except np.linalg.LinAlgError:
                coef = np.polyfit(x, y, 1, cov=False)
                trend_line = np.poly1d(coef)(x)
                trend_label = '近似趋势线'

            sns.pointplot(
                x=binned_col,
                y='坏样本率',
                data=stats,
                color=COLOR_BAD,
                scale=0.7,
                ax=ax2
            )
            ax2.plot(x, trend_line, '--', color='grey', alpha=0.7, label=trend_label)
            ax2.legend()

            # 格式调整
            ax2.set_xticklabels(
                stats[binned_col].astype(str),
                rotation=45,
                ha='right'
            )
            ax2.set_ylim(0, 1)
            ax2.set_title('分箱坏样本率趋势', fontsize=12)
            plt.tight_layout()

            # 保存输出
            plt.savefig(f"{save_dir}/{col}.png", dpi=120, bbox_inches='tight')
            plt.close()

            # 保存数据
            stats.to_csv(f"{save_dir}/{col}.csv", index=False, encoding='utf_8_sig')

        except Exception as e:
            print(f"分析 {col} 时发生异常: {str(e)}")
            continue

#%% 主流程
if __name__ == '__main__':
    # 数据加载
    raw_df = load_data('original_data.csv')

    # 数据清洗
    processed_df = clean_data(raw_df)

    # 分析执行
    analyze_discrete(processed_df)
    analyze_continuous(processed_df)

    # 结果保存
    processed_df.to_csv('processed_data.csv', index=False, encoding='utf_8_sig')
    print("分析完成！结果已保存至：")
    print(f"离散变量分析: {os.path.abspath('离散变量分析')}")
    print(f"连续变量分析: {os.path.abspath('连续变量分析')}")
    print(f"清洗后数据: processed_data.csv")


# In[ ]:




